package cn.dansj.utils.minio;

import cn.dansj.bean.FileUploadInfo;
import cn.dansj.common.springboot.file.FileUtils;
import cn.dansj.minio.CustomMinioClient;
import cn.dansj.common.utils.bean.ResponseMessage;
import cn.dansj.common.utils.bean.Return;
import cn.dansj.common.utils.http.UrlUtils;
import cn.dansj.common.utils.transfer.AssertUtils;
import cn.dansj.common.utils.transfer.GetTime;
import cn.dansj.common.utils.transfer.Transformation;
import cn.dansj.common.utils.transfer.Verification;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.admin.MinioAdminClient;
import io.minio.admin.QuotaUnit;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import io.minio.messages.Part;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class MinioUtils {
    private final Logger logger = LoggerFactory.getLogger(MinioUtils.class);

    private final CustomMinioClient minioClient;
    private final MinioAdminClient minioAdminClient;
    private static final ResponseMessage responseMessage = ResponseMessage.getInstance();

    private MinioUtils(CustomMinioClient minioClient, MinioAdminClient minioAdminClient) {
        this.minioClient = minioClient;
        this.minioAdminClient = minioAdminClient;
    }

    public static MinioUtils getInstance(CustomMinioClient minioClient, MinioAdminClient minioAdminClient) {
        return new MinioUtils(minioClient, minioAdminClient);
    }

    public static MinioUtils getInstance(String endpoint, String accessKey, String secretKey) {
        return new MinioUtils(new CustomMinioClient(MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build()),
                MinioAdminClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build());
    }

    public boolean isBucket(String bucket) {
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        } catch (Exception e) {
            return false;
        }
    }

    public boolean copy(String sourceBucket, String sourceObject, String targetBucket, String targetObject) {
        try {
            final CopySource copySource = CopySource.builder().bucket(sourceBucket).object(sourceObject).build();
            minioClient.copyObject(CopyObjectArgs.builder().source(copySource).bucket(targetBucket).object(targetObject).build());
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    public ResponseMessage createBucket(String bucket) {
        boolean bucketExists = isBucket(bucket);
        if (bucketExists) {
            return responseMessage.responseCode(1).message("bucket already exists");
        } else {
            try {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
                return responseMessage.responseCode(1).message("create bucket successful");
            } catch (Exception e) {
                return responseMessage.responseCode(0).message(e.getMessage());
            }
        }
    }

    /**
     * 初始化分片上传
     */
    public Return<Map<String, Object>> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int chunkNum, String contentType, String bucketName) {
        logger.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, chunkNum, contentType, bucketName);
        Map<String, Object> resMap = new HashMap<>();

        try {

            if (Verification.checkNull(contentType)) {
                contentType = "application/octet-stream";
            }
            HashMultimap<String, String> headers = HashMultimap.create();

            headers.put("Content-Type", contentType);

            //获取uploadId
            String uploadId;
            if (Verification.checkNull(fileUploadInfo.getUploadId())) {
                uploadId = minioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
            } else {
                uploadId = fileUploadInfo.getUploadId();
            }

            resMap.put("uploadId", uploadId);

            fileUploadInfo.setUploadId(uploadId);
            fileUploadInfo.setChunkNum(chunkNum);

            List<String> partList = new ArrayList<>();

            Map<String, String> reqParams = new HashMap<>();
            reqParams.put("uploadId", uploadId);
            reqParams.put("signature", "true");
            for (int i = 1; i <= chunkNum; i++) {
                reqParams.put("partNumber", String.valueOf(i));
                String uploadUrl = minioClient.getPresignedObjectUrl(
                        GetPresignedObjectUrlArgs.builder()
                                .method(Method.PUT)
                                .bucket(bucketName)
                                .object(objectName)
                                .expiry(600, TimeUnit.SECONDS)
                                .extraQueryParams(reqParams)
                                .build());
                AssertUtils.state(uploadUrl.startsWith("http"), "上传地址获取失败");
                partList.add(uploadUrl);
            }
            logger.info("tip message: 文件初始化<分片上传>、成功");
            resMap.put("urlList", partList);
            return Return.T(resMap);
        } catch (Exception e) {
            logger.error("error message: 初始化分片上传失败、原因:", e);
            // 返回 文件上传失败
            resMap.clear();
            resMap.put("code", 1002);
            resMap.put("message", "文件上传失败");
            return Return.T(resMap).setStatus(false);
        }
    }

    /**
     * 分片上传完后合并
     *
     * @param objectName 文件全路径名称
     * @param uploadId   返回的uploadId
     * @param bucketName 桶名称
     * @return boolean
     */
    public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
        try {
            logger.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
            createBucket(bucketName);
            //目前仅做了最大1000分片
            Part[] parts = new Part[1000];
            // 查询上传后的分片数据
            ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
            int partNumber = 1;
            for (Part part : partResult.result().partList()) {
                parts[partNumber - 1] = new Part(partNumber, part.etag());
                partNumber++;
            }
            // 合并分片
            minioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);

        } catch (Exception e) {
            logger.error("error message: 合并失败、原因:", e);
            //TODO删除redis的数据
            return false;
        }
        return true;
    }

    /**
     * 通过 sha256 获取上传中的分片信息
     *
     * @param objectName 文件全路径名称
     * @param uploadId   返回的uploadId
     * @param bucketName 桶名称
     * @return Mono<Map < String, Object>>
     */
    public List<Integer> getChunkByFileMD5(String objectName, String uploadId, String bucketName) {
        logger.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
        try {
            // 查询上传后的分片数据
            ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
            return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
        } catch (Exception e) {
            logger.error("error message: 查询上传后的分片信息失败、原因:", e);
            return null;
        }
    }

    public ResponseMessage upload(String bucket, File file, String pathInBucket, String filename) {
        return upload(bucket, FileUtils.convertFileToMultipartFile(file), pathInBucket, filename);
    }

    public ResponseMessage upload(String bucket, File file) {
        return upload(bucket, file, null, null);
    }

    public ResponseMessage upload(String bucket, File file, String filename) {
        return upload(bucket, file, null, filename);
    }

    public ResponseMessage upload(String bucket, MultipartFile file, String filename) {
        return upload(bucket, file, null, filename);
    }

    public ResponseMessage upload(String bucket, MultipartFile file, String pathInBucket, String filename) {
        ResponseMessage create = createBucket(bucket);
        if (!create.getStatus()) return create;
        try {
            String name = (Verification.checkNotNull(pathInBucket) ? pathInBucket + "/" : "") + Transformation.nvl(filename, file.getOriginalFilename());
            PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(UrlUtils.getMinioUrl(name)).contentType(file.getContentType())
                    .stream(file.getInputStream(), file.getSize(), PutObjectArgs.MIN_MULTIPART_SIZE).build();
            minioClient.putObject(putObjectArgs);
            return responseMessage.responseCode(1).message("upload successful");
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    public ResponseMessage delete(String bucket, String filename) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(UrlUtils.getMinioUrl(filename)).build());
            return responseMessage.responseCode(1).message("删除成功");
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    public void deleteByPrefix(String bucket, String prefix) {
        List<Item> fileItem = getFileItem(bucket, prefix, true);
        for (Item item : fileItem) {
            try {
                minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(UrlUtils.getMinioUrl(item.objectName())).build());
            } catch (Exception ignore) {
            }
        }

    }

    public ResponseMessage delete(String bucket, final Collection<String> filename) {
        List<DeleteObject> deleteObjects = new ArrayList<>();
        filename.forEach(e -> deleteObjects.add(new DeleteObject(UrlUtils.getMinioUrl(e))));
        return delete(bucket, deleteObjects);
    }

    public ResponseMessage delete(String bucket, final Iterable<DeleteObject> filename) {
        try {
            minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucket).objects(filename).build());
            return responseMessage.responseCode(1).message("删除成功");
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    public ResponseMessage removeBucket(String bucket) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
            return responseMessage.responseCode(1).message("删除成功");
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    /**
     * 获取文件分享链接
     */
    public ResponseMessage share(String bucket, String filename, int maxExpireDay) {
        try {
            //先判断文件是否存在,不存在则不返回url
            StatObjectResponse statObjectResponse = fileInfo(bucket, filename);
            if (statObjectResponse == null) {
                return responseMessage.responseCode(0).message("文件不存在, 无法分享");
            }
            //生成url信息
            GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
                    .expiry(maxExpireDay, TimeUnit.DAYS).bucket(bucket)
                    .object(UrlUtils.getMinioUrl(filename)).method(Method.GET).build();
            String url = minioClient.getPresignedObjectUrl(urlArgs);
            return responseMessage.responseCode(1).message(url);
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    public ResponseMessage getUploadUrl(String bucket, String filename) {
        try {
            //生成url信息
            GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
                    .expiry(600, TimeUnit.SECONDS).bucket(bucket).extraQueryParams(Map.of("signature", "true"))
                    .object(UrlUtils.getMinioUrl(filename)).method(Method.PUT).build();
            String url = minioClient.getPresignedObjectUrl(urlArgs);
            return responseMessage.responseCode(1).message(url);
        } catch (Exception e) {
            return responseMessage.responseCode(0).message(e.getMessage());
        }
    }

    /**
     * 获取文件信息，判断文件是否存在
     *
     * @param bucket   存储桶
     * @param filename 文件名
     */
    public StatObjectResponse fileInfo(String bucket, String filename) {
        try {
            return minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(UrlUtils.getMinioUrl(filename)).build());
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 判断文件夹是否存在
     *
     * @param bucketName 存储桶
     * @param objectName 文件夹名称
     */
    public boolean isDir(String bucketName, String objectName) {
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(UrlUtils.getMinioUrl(objectName)).recursive(false).build());
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && UrlUtils.getMinioUrl(objectName).equals(UrlUtils.getMinioUrl(item.objectName()))) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return exist;
    }

    public InputStream download(String bucket, String filename) {
        try {
            return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(UrlUtils.getMinioUrl(filename)).build());
        } catch (Exception e) {
            throw new RuntimeException("error downloading file from server", e);
        }
    }

    public ResponseMessage download(String bucket, String minioPath, String localPath) {
        int fileNum = 0;
        try {
            boolean bucketExists = isBucket(bucket);
            if (!bucketExists) {
                return responseMessage.responseCode(0).message("bucket[" + bucket + "]不存在");
            } else {
                fileNum = download(bucket, UrlUtils.getMinioUrl(minioPath), localPath.endsWith(File.separator) ? localPath : localPath + File.separator, fileNum);
                return responseMessage.responseCode(1).message("下载成功, " + "minio目录地址: " + minioPath + ", 本地目录地址: " + localPath + ", 文件数: " + fileNum);
            }
        } catch (Exception e) {
            return responseMessage.responseCode(0).message("下载失败:" + Transformation.getBaseException(e).getMessage());
        }
    }

    @SuppressWarnings("all")
    private int download(String bucket, String minioPath, String localPath, int fileNum) throws MinioException, IOException, NoSuchAlgorithmException, InvalidKeyException, ServerException, InsufficientDataException, ErrorResponseException, InvalidResponseException, XmlParserException, InternalException {
        Iterable<Result<Item>> listObjects = minioClient.listObjects(ListObjectsArgs.builder().prefix(UrlUtils.getMinioUrl(minioPath)).bucket(bucket).recursive(false).build());
        for (Result<Item> result : listObjects) {
            Item item = result.get();
            File localFile = new File(localPath + item.objectName());
            if (item.isDir()) {
                if (!localFile.exists()) {
                    localFile.mkdirs();
                }
                fileNum = download(bucket, item.objectName(), localPath, fileNum);
            } else {
                fileNum += 1;
                if (!localFile.exists()) {
                    if (!localFile.getParentFile().exists()) {
                        localFile.getParentFile().mkdirs();
                    }
                    localFile.createNewFile();
                }
                minioClient.downloadObject(
                        DownloadObjectArgs.builder()
                                .bucket(bucket)
                                .object(item.objectName())
                                .filename(localFile.getAbsolutePath())
                                .build());
            }
        }
        return fileNum;
    }

    public long getBucketQuota(String bucket) {
        try {
            return minioAdminClient.getBucketQuota(bucket);
        } catch (Exception e) {
            return 0;
        }
    }

    private static class BucketInfo {
        public final long size;
        public final int files;

        public BucketInfo(long size, int files) {
            this.size = size;
            this.files = files;
        }
    }

    private BucketInfo getBucketInfo(String bucket) {
        List<Item> fileItem = getFileItem(bucket, "", true);
        long size = 0;
        int file = 0;
        for (Item item : fileItem) {
            size += item.size();
            file += 1;
        }
        return new BucketInfo(size, file);
    }

    public List<Map<String, Object>> getBucket() {
        try {
            return minioClient.listBuckets().stream().map(e -> {
                Map<String, Object> bucketInfo = new HashMap<>();
                bucketInfo.put("name", e.name());
                bucketInfo.put("creationDate", e.creationDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Shanghai"))));
                bucketInfo.put("policy", getBucketPolicy(e.name()));
                bucketInfo.put("quota", getBucketQuota(e.name()));
                BucketInfo info = getBucketInfo(e.name());
                bucketInfo.put("size", info.size);
                bucketInfo.put("files", info.files);
                return bucketInfo;
            }).sorted((bucket1, bucket2) -> {
                try {
                    return GetTime.dateTime(bucket1.get("creationDate").toString()).compareTo(GetTime.dateTime(bucket2.get("creationDate").toString()));
                } catch (Exception exception) {
                    return 1;
                }
            }).collect(Collectors.toList());
        } catch (Exception e) {
            return new ArrayList<>();
        }
    }

    public boolean setQuota(String bucket, long size, String unit) {
        try {
            if (size > 0) {
                minioAdminClient.setBucketQuota(bucket, size, QuotaUnit.valueOf(unit));
            } else {
                minioAdminClient.clearBucketQuota(bucket);
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private List<Item> getFileItem(String bucket, String prefix, boolean recursive) {
        try {
            List<Item> list = new ArrayList<>();
            Iterable<Result<Item>> objects = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucket).prefix(UrlUtils.getMinioUrl(prefix)).recursive(recursive).build());
            if (objects != null) {
                for (Result<Item> item : objects) {
                    list.add(item.get());
                }
            }
            return list;
        } catch (Exception exception) {
            return new ArrayList<>();
        }
    }

    /**
     * 根据文件前缀查询文件
     */
    public List<Map<String, Object>> getFile(String bucket, String prefix, boolean recursive) {
        List<Item> fileItem = getFileItem(bucket, prefix, recursive);
        return fileItem.stream().map(item -> {
            Map<String, Object> file = new HashMap<>();
             /*
             file.put("etag", StringUtil.replace(item.etag(), "\"", ""));
             file.put("isDir", item.isDir());
             file.put("isDeleteMarker", item.isDeleteMarker());
             file.put("isLatest", item.isLatest());
             file.put("owner", item.owner());
             file.put("versionId", item.versionId());
             **/
            file.put("objectName", item.objectName());
            file.put("size", item.size());
            file.put("lastModified", item.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Shanghai"))));
            file.put("showName", item.objectName().contains("/") ? item.objectName().substring(item.objectName().lastIndexOf("/") + 1) : item.objectName());
            file.put("bucket", bucket);
            return file;
        }).collect(Collectors.toList());
    }

    public String getFilePath(String bucketName, String fileName) {
        String endpoint = minioClient.getEndpoint();
        return String.format("%s/%s/%s", endpoint, bucketName, fileName);//文件访问路径
    }

    /**
     * 更新桶权限策略
     *
     * @param bucket 桶
     * @param policy 权限
     */
    public boolean setBucketPolicy(String bucket, MinioBucketPolicy policy) {
        try {
            switch (policy) {
                case readOnly:
                    minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(MinioBucketPolicy.readOnly.getPolicy(bucket)).build());
                    break;
                case writeOnly:
                    minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(MinioBucketPolicy.writeOnly.getPolicy(bucket)).build());
                    break;
                case readWrite:
                    minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(MinioBucketPolicy.readWrite.getPolicy(bucket)).build());
                    break;
                case forbidVisit:
                    minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(MinioBucketPolicy.forbidVisit.getPolicy(bucket)).build());
                    break;
                default:
                    break;
            }
            return true;
        } catch (Exception ignore) {
            return false;
        }
    }

    public String getBucketPolicy(String bucket) {
        try {
            return MinioBucketPolicy.get(bucket, minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucket).build())).getName();
        } catch (Exception exception) {
            return MinioBucketPolicy.forbidVisit.getName();
        }
    }
}
